47. Spring Security 演示

搭建演示项目

搭建 web 工程

创建 Maven war 类型的项目,创建 web.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
version="2.5">
<display-name>spring-security-01</display-name>
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>

<!-- The front controller of this Spring Web application, responsible for
handling all application requests -->
<servlet>
<servlet-name>springDispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>

<!-- Map all requests to the DispatcherServlet for handling -->
<servlet-mapping>
<servlet-name>springDispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>

</web-app>

pom 文件中引入相关依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.atguigu.maven</groupId>
<artifactId>SpringSucurity-HelloWord</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>

<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>4.3.20.RELEASE</version>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.2</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
</dependencies>
</project>

进行 spring 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.3.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd">
<context:component-scan
base-package="com.atguigu.security"></context:component-scan>
<bean
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/"></property>
<property name="suffix" value=".jsp"></property>
</bean>
<mvc:annotation-driven />
<mvc:default-servlet-handler />
</beans>

引入页面和 controller

直接复制页面到 webapp 下面,复制 controller 包下面。

启动项目,访问首页

这时就能看到项目正常启动,且能正常访问了。

引入 SpringSecurity

在 pom 文件中添加下面依赖。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- SpringSecurity -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>4.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>4.2.10.RELEASE</version>
</dependency>
<!-- 标签库 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-taglibs</artifactId>
<version>4.2.10.RELEASE</version>
</dependency>

继承 WebSecurityConfigurerAdapter

创建一个类让其继承自 WebSecurityConfigurerAdapter。

1
2
3
4
5
6
7
8
9
10
11
12
package com.atguigu.security.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration // 声明当前类是一个配置类,相当于 xml 配置文件作用
@EnableWebSecurity // 启用 SpringSecurity 安全机制
public class AppWebSecurityConfig extends WebSecurityConfigurerAdapter {

}

再次访问我们的页面,回发现我们的所有请求都被拦截了,并重定向到了一个默认的 login 登录页面。

实验1 - 授权首页和静态资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.atguigu.security.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration // 声明当前类是一个配置类,相当于 xml 配置文件作用
@EnableWebSecurity // 启用 SpringSecurity 安全机制
public class AppWebSecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
// 默认规则,所有请求都受到限制,并且会跳转到自动生成的login 页面
// super.configure(http);

// 实验1,授权放行登录页面和静态规则(默认规则现在两者都不能访问)。其他的全部进行拦截
http.authorizeRequests().antMatchers("/layui/**", "/index.jsp").permitAll().anyRequest().authenticated();
}
}

这个时候:

  1. 如果访问 http://localhost:8080/SpringSucurity-HelloWord/ 或者 http://localhost:8080/SpringSucurity-HelloWord/index.jsp 或者 http://localhost:8080/SpringSucurity-HelloWord/layui/layui.js 都是可以访问的。
  2. 如果访问 layui 下不存在的资源,那么会提示 404 错误(有权限访问,但是资源不存在)。
  3. 但是随便访问一个不存在的地址,比如 http://localhost:8080/SpringSucurity-HelloWord/xx 那么就会提示 403 Forbidden

实验2 - 默认及自定义登录页

默认登录页

启用默认的登录页,让提示 403 的访问都去到默认的登录页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.atguigu.security.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration // 声明当前类是一个配置类,相当于 xml 配置文件作用
@EnableWebSecurity // 启用 SpringSecurity 安全机制
public class AppWebSecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
// 默认规则,所有请求都受到限制,并且会跳转到自动生成的login 页面
// super.configure(http);

// 实验1,授权放行登录页面和静态规则(默认规则现在两者都不能访问)。其他的全部进行拦截
http.authorizeRequests().antMatchers("/layui/**", "/index.jsp").permitAll().anyRequest().authenticated();

// 实验2,默认及自定义登录页面
// 对于无权限访问(403)的地址,那么跳转默认的登录页面。404 资源不存在是不会跳转的(404数据有权限,只是资源不存在)
http.formLogin();
}
}

自定义登录页

可以指定单独的登录页面,并且可以指定账号参数名称和密码参数名称。可以指定登录请求处理地址。还可以禁用 csrf。

注意⚠️:默认的请求处理地址是 /login。账号参数名称为 username,密码参数名称为 password。请求方式是 POST,而且需要加上 csrf。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.atguigu.security.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration // 声明当前类是一个配置类,相当于 xml 配置文件作用
@EnableWebSecurity // 启用 SpringSecurity 安全机制
public class AppWebSecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
// 默认规则,所有请求都受到限制,并且会跳转到自动生成的login 页面
// super.configure(http);

// 实验1,授权放行登录页面和静态规则(默认规则现在两者都不能访问)。其他的全部进行拦截
http.authorizeRequests().antMatchers("/layui/**", "/index.jsp").permitAll().anyRequest().authenticated();

// 实验2,默认及自定义登录页面
// 对于无权限访问(403)的地址,那么跳转默认的登录页面。404 资源不存在是不会跳转的(404数据有权限,只是资源不存在)
// http.formLogin();

// 自定义登录页。页面地址是 /index.jsp,账号参数名称是 loginacct,密码参数名称是 userpswd,处理请求的地址是 /doLogin。
// 认证成功后跳转到 /main.html 这个地址。(注意需要加上 _csrf)
http.formLogin()
.loginPage("/index.jsp")
.usernameParameter("loginacct")
.passwordParameter("userpswd")
.loginProcessingUrl("/doLogin")
.defaultSuccessUrl("/main.html");

// 关闭 csrf
http.csrf().disable();
}
}

可以在页面中使用 ${SPRING_SECURITY_LAST_EXCEPTION.message} 来获取错误信息

实验3 - 自定义认证

这里演示基于内存的认证方式。重写 configure 方法,参数是 AuthenticationManagerBuilder 类型的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package com.atguigu.security.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration // 声明当前类是一个配置类,相当于 xml 配置文件作用
@EnableWebSecurity // 启用 SpringSecurity 安全机制
public class AppWebSecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 默认规则
// super.configure(auth);

// 实验3-自定义认证。基于内存的认证方式,有 zhangsan 和 lisisi 两个用户。zhangsan 的角色是学徒,lisisi拥有的权限是 "罗汉拳" 和 "吸星大法"
auth.inMemoryAuthentication().withUser("zhangsan").password("123456").roles("学徒")
.and()
.withUser("lisisi").password("123").authorities("罗汉拳", "吸星大法");
}

@Override
protected void configure(HttpSecurity http) throws Exception {
// 默认规则,所有请求都受到限制,并且会跳转到自动生成的login 页面
// super.configure(http);

// 实验1,授权放行登录页面和静态规则(默认规则现在两者都不能访问)。其他的全部进行拦截
http.authorizeRequests().antMatchers("/layui/**", "/index.jsp").permitAll().anyRequest().authenticated();

// 实验2,默认及自定义登录页面
// 对于无权限访问(403)的地址,那么跳转默认的登录页面。404 资源不存在是不会跳转的(404数据有权限,只是资源不存在)
// http.formLogin();

// 自定义登录页。页面地址是 /index.jsp,账号参数名称是 loginacct,密码参数名称是 userpswd,处理请求的地址是 /doLogin。
// 认证成功后跳转到 /main.html 这个地址。(注意需要加上 _csrf)
http.formLogin()
.loginPage("/index.jsp")
.usernameParameter("loginacct")
.passwordParameter("userpswd")
.loginProcessingUrl("/doLogin")
.defaultSuccessUrl("/main.html");

// 关闭 csrf
http.csrf().disable();

}
}

使用 zhangsan 和 lisisi 就能登录成功了。

所有的表单提交为了防止跨站请求伪造,我们需要加上隐藏域 _csrf

<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>

或者暂时禁用 http.csrf().disable(); 上面的例子中我们是禁用的,现在我们可以不禁用,只要在表单中加上上面的隐藏 input 内容即可。

如果不禁用 csrf,默认是开启的状态,页面不设置 csrf 表单域,那么提交登录请求会报错

csrf token 值的变化:

  1. 如果登录成功(用户名,密码正确), 令牌会被删除。
  2. 重新回到登录页或后退网页, 令牌会重新生成
  3. 如果登录失败(用户名,密码错误), 令牌不变。
  4. 刷新登录页, 令牌值也不变

实验4 - 注销

在 configure 方法中可以开启注销,可以使用默认的或者自定义的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package com.atguigu.security.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration // 声明当前类是一个配置类,相当于 xml 配置文件作用
@EnableWebSecurity // 启用 SpringSecurity 安全机制
public class AppWebSecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
// 默认规则,所有请求都受到限制,并且会跳转到自动生成的login 页面
// super.configure(http);

// 实验1,授权放行登录页面和静态规则(默认规则现在两者都不能访问)。其他的全部进行拦截
http.authorizeRequests().antMatchers("/layui/**", "/index.jsp").permitAll().anyRequest().authenticated();

// 实验2,默认及自定义登录页面
// 对于无权限访问(403)的地址,那么跳转默认的登录页面。404 资源不存在是不会跳转的(404数据有权限,只是资源不存在)
// http.formLogin();

// 自定义登录页。页面地址是 /index.jsp,账号参数名称是 loginacct,密码参数名称是 userpswd,处理请求的地址是 /doLogin。
// 认证成功后跳转到 /main.html 这个地址。(注意需要加上 _csrf)
http.formLogin()
.loginPage("/index.jsp")
.usernameParameter("loginacct")
.passwordParameter("userpswd")
.loginProcessingUrl("/doLogin")
.defaultSuccessUrl("/main.html");

// 关闭 csrf
// http.csrf().disable();

// 实验4 - 注销
// 默认注销规则,路径是 /logout,如果 csrf 开启,必须 post 方式的 /logout 请求,表单中需要增加 csrf token
// http.logout();

// 自定义注销。设置自定义路径是 /doLogout,注销成功后跳转到 /index.jsp
http.logout().logoutUrl("/doLogout").logoutSuccessUrl("/index.jsp");
}
}

页面中内容如下:

1
2
3
4
<form id="logoutForm" action="${PATH }/doLogout" method="POST">
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
<a onclick="$('#logoutForm').submit()">退出</a>
</form>

实验5 - 基于角色的访问控制

我们可以设置 level1 下的所有资源只能 “学徒” 角色能访问,level2 下的资源只能 “大师” 能访问,level3 下的资源只能 “宗师” 能访问。其余的登录后都可以访问。

1
2
3
4
5
6
// 实验5,基于角色的访问控制
http.authorizeRequests().antMatchers("/layui/**", "/index.jsp").permitAll()
.antMatchers("/level1/**").hasRole("学徒")
.antMatchers("/level2/**").hasRole("大师")
.antMatchers("/level3/**").hasRole("宗师")
.anyRequest().authenticated();

因为上面已经为 zhangsan 这个用户登录后赋予 “学徒” 的角色,所以 zhangsan 登录后就能访问 level1 下的所有资源了。

1
2
// 基于内存的认证方式,有 zhangsan 和 lisisi 两个用户。zhangsan 的角色是学徒,lisisi拥有的权限是 "罗汉拳" 和 "吸星大法"
auth.inMemoryAuthentication().withUser("zhangsan").password("123456").roles("学徒").and().withUser("lisisi").password("123").authorities("罗汉拳", "吸星大法");

访问效果如下所示:

实验6 - 自定义访问拒绝处理页面

让出现 403 的错误都跳转到 /unauth.html

1
2
// 实验6 - 自定义访问拒绝处理页面
http.exceptionHandling().accessDeniedPage("/unauth.html");

controller 新增处理方法,并新建 unauth.html 页面

1
2
3
4
@GetMapping("/unauth.html")
public String unauth(){
return "unauth";
}

此时再次访问无权限的地址,就会跳转到 unauth.html 页面了

实验7 - 记住我

开启 “记住我” 的功能

1
2
// 实验7 - 记住我
http.rememberMe();

提交表单的时候需要提交 “remember-me” 参数

1
<input type="checkbox" name="remember-me" lay-skin="primary" title="记住密码">

默认会记住2周登录状态,会在 cookie 中保存名为,remember-me 的 cookie。登录后页面,关闭浏览器,再次打开浏览器,直接访问就不需要再次登录了。这种方式,token值是放置在内存中的,服务器端重启 tomcat, token 会失效。需要将token记录在数据库持久化才不会失效。

基于数据库

引入依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>4.3.20.RELEASE</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.12</version>
</dependency>
<!-- mysql驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>

配置数据源

1
2
3
4
5
6
7
8
9
10
11
12
<!-- 配置数据源 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="username" value="root"></property>
<property name="password" value="root"></property>
<property name="url" value="jdbc:mysql://192.168.137.3:3306/security?useSSL=false"></property>
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
</bean> 

<!-- jdbcTemplate-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>

创建数据库表 (SQL 语句是框架中的)

1
create table persistent_logins (username varchar(64) not null, series varchar(64) primary key,token varchar(64) not null, last_used timestamp not null)

加上以下代码就能实现基于数据库的记住我功能(前端同样也要传递 remember-me 参数)

1
2
3
4
// 实验7 - 记住我 基于数据库
JdbcTokenRepositoryImpl ptr = new JdbcTokenRepositoryImpl();
ptr.setDataSource(dataSource);
http.rememberMe().tokenRepository(ptr);

再次测试就会发现能实现记住我功能了,且服务器重启不影响记住我的功能。且会在刚刚创建的表里面看见了新插入的一条数据。

认证

密码加密进行校验

根据 security.sql 文件创建表

实现 UserDetailService 接口 loadUserByUsername(String username) 方法。并在里面进行用户查询,权限的查询。并将查询到的信息创建 User 对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.atguigu.security.component;

import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;

@Component
public class AppUserDetailsServiceImpl implements UserDetailsService {
@Autowired
JdbcTemplate jdbcTemplate;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
String queryUser = "SELECT * FROM `t_admin` WHERE loginacct=?";

//1、查询指定用户的信息
Map<String, Object> map = jdbcTemplate.queryForMap(queryUser, username);

//2、将查询到的用户封装到框架使用的 UserDetails 里面
return new User(map.get("loginacct").toString(), map.get("userpswd").toString(),
AuthorityUtils.createAuthorityList("ROLE_学徒","ROLE_大师", "ADMIN"));
//暂时写死,过后数据库中查。如果是角色的话,需要加上 ROLE_ 前缀。如果是权限则不用加
}
}

在重写的 configure 方法中启用自定义的查询认证方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;

@Configuration // 声明当前类是一个配置类,相当于 xml 配置文件作用
@EnableWebSecurity // 启用 SpringSecurity 安全机制
public class AppWebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
DataSource dataSource;

@Autowired
UserDetailsService userDetailsService;

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 默认规则
// super.configure(auth);

//根据用户名查询出用户的详细信息
auth.userDetailsService(userDetailsService);
}
}

在数据中新增 zhangsan 的用户,设置密码为 123456。因为这里给予的权限是写死的 学徒 和 大师,所以访问现象和以前基于内存是一样的。

采用 MD5 加密

定义 AppPasswordEncoder 类,实现 PasswordEncoder 接口,并复写其中的两个方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.atguigu.security.component;

import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

@Component
public class AppPasswordEncoder implements PasswordEncoder {

@Override
public String encode(CharSequence rawPassword) {
// rawPassword 为密码的原文
return MD5Util.digest(rawPassword.toString());
}

@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
// 比较密码是否一致
// rawPassword 是传入的密码的原文, encodedPassword 是数据库中查询的来的密码信息
return encodedPassword.equals(encode(rawPassword));
}
}
1
2
// 采用 md5 的方式进行比较
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);

将数据库中的 zhangsan 的密码改为 123456 其对应的 md5 值,测试登录后没问题。

采用 BCrypt 加密

采用 BCrypt 都不需要实现 PasswordEncoder 接口。直接传入一个 BCryptPasswordEncoder 对象即可。

1
2
// 采用 BCrypt 的方式进行比较
auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());

BCrypt 加密出来的数据每次都是不一样的,但其实原理就是虽然对同一个密码,每次生成的hash不一样,但是hash 中包含了 salt(hash产生过程:先随机生成salt,salt跟 password 进行 hash)在下次校验时,从hash中取出 salt,salt 跟 password 进行 hash。得到的结果跟保存在 DB 中的 hash 进行比对即可。

将数据库中的 zhangsan 的密码改为 123456 其对应的 BCrypt 值,测试登录后没问题。

细粒度的权限控制

上面的设计中,我们限制了 /level/1 下的所有菜单只要拥有学徒权限都可以访问,这还不是很细致。我们可以控制 controller 中每个方法的权限。在 AppWebSecurityConfig 类中加上 @EnableGlobalMethodSecurity(prePostEnabled = true) 注解,并去掉之前给每个 url 设置的权限信息

1
2
3
4
5
http.authorizeRequests().antMatchers("/layui/**", "/index.jsp").permitAll()
// .antMatchers("/level1/**").hasRole("学徒")
// .antMatchers("/level2/**").hasRole("大师")
// .antMatchers("/level3/**").hasRole("宗师")
.anyRequest().authenticated();

@EnableGlobalMethodSecurity(prePostEnabled = true) 就是开启细粒度的全局方法级别的权限控制,即可以为每个 controller 中的方法设置权限信息。为 GongfuController 中不同的 url 地址设置不同的权限信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@Controller
public class GongfuController {
// 为 /level1/1 设置只有学徒角色和 luohan 的权限才能访问
@PreAuthorize("hasRole('学徒') AND hasAuthority('luohan')")
@GetMapping("/level1/1")
public String leve1Page1(){
return "/level1/1";
}

// 为 /level1/2 设置只有学徒角色和 wudang 的权限才能访问
@PreAuthorize("hasRole('学徒') AND hasAuthority('wudang')")
@GetMapping("/level1/2")
public String leve1Page2(){
return "/level1/2";
}

// 为 /level1/3 设置只有学徒角色和 quanzhen 的权限才能访问
@PreAuthorize("hasRole('学徒') AND hasAuthority('quanzhen')")
@GetMapping("/level1/3")
public String leve1Page3(){
return "/level1/3";
}
}

重写 UserDetailsService 中查询用户信息的方法(查询用户有什么权限,有什么角色,如果有对应 controller 方法上的角色和权限则可以访问该方法)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
package com.atguigu.security.component;

import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.ColumnMapRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

@Component
public class AppUserDetailsServiceImpl implements UserDetailsService {
@Autowired
JdbcTemplate jdbcTemplate;

// @Override
// public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// String queryUser = "SELECT * FROM `t_admin` WHERE loginacct=?";
//
// //1、查询指定用户的信息
// Map<String, Object> map = jdbcTemplate.queryForMap(queryUser, username);
//
// //2、将查询到的用户封装到框架使用的 UserDetails 里面
// return new User(map.get("loginacct").toString(), map.get("userpswd").toString(),
// AuthorityUtils.createAuthorityList("ROLE_学徒","ROLE_大师", "ADMIN"));
// //暂时写死,过后数据库中查。如果是角色的话,需要加上 ROLE_ 前缀。如果是权限则不用加
// }

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
String sql = "select * from t_admin where loginacct=?";
Map<String, Object> map = jdbcTemplate.queryForMap(sql, username);

// 查询用户拥有的角色集合
String sql1 = "SELECT t_role.* FROM t_role LEFT JOIN t_admin_role ON t_admin_role.roleid=t_role.id WHERE t_admin_role.adminid=?";
List<Map<String, Object>> roleList = jdbcTemplate.query(sql1, new ColumnMapRowMapper(), map.get("id"));

// 查询用户拥有的权限集合
String sql2 = "SELECT distinct t_permission.* FROM t_permission LEFT JOIN t_role_permission ON t_role_permission.permissionid = t_permission.id LEFT JOIN t_admin_role ON t_admin_role.roleid=t_role_permission.roleid WHERE t_admin_role.adminid=?";
List<Map<String, Object>> permissionList = jdbcTemplate.query(sql2, new ColumnMapRowMapper(), map.get("id"));

// 用户权限=【角色+权限】
Set<GrantedAuthority> authorities = new HashSet<GrantedAuthority>();

for (Map<String, Object> rolemap : roleList) {
String rolename = rolemap.get("name").toString();
authorities.add(new SimpleGrantedAuthority("ROLE_" + rolename));
}

for (Map<String, Object> permissionmap : permissionList) {
String permissionName = permissionmap.get("name").toString();
if (!StringUtils.isEmpty(permissionName)) {
authorities.add(new SimpleGrantedAuthority(permissionName));
}
}
return new User(map.get("loginacct").toString(), map.get("userpswd").toString(), authorities);
}
}

为 zhangsan 赋予赋予学徒的角色,并为学徒角色赋予 luohan 的权限。

将角色赋予罗汉拳的权限

此时再次访问系统,因为 zhangsan 只有学徒的角色和罗汉拳的权限,所以能访问罗汉拳的页面。

注意⚠️: